今天接續往下作控制元件。目標打算實做登入邏輯。
為了能讓元件與邏輯具備各自獨立。這邊將使用透過 React 的 ContextProvider 的方式,來實做登入邏輯。使用 ReactNative 的 AsyncStorage 來儲存 access_token, refresh_token 與 User 的資訊。透過 useEffect 來從 AsyncStorage 更新當下 state 狀態。
這次的實做步驟,會先從一些資料存取的邏輯開始實做。然後,最後再套用在 Provider 元件對應的生命周期函數上。
這部份邏輯會實做在 userService ,而型別會特別獨立出一個 types 資料夾存放
這邊打算透過 axios 套件來作 http request 的實做
npm i -S axios
這邊會利用 axios 的 interceptor 來作一些基礎的設定,比如像是自動帶入 access_token 到 header 內。
export type ApiResponse<T> = {
message: string;
error?: string;
data: T;
statusCode?: number;
}
這邊使用 Generic Type 的宣告方式,來讓 data 的型別會透過帶入 T 來定義。保留回傳值得彈性。
透過 ApiResponse ,可以方便的定義出其他回應的型別 AuthResponse, RegisterResponse。
import { ApiResponse } from './api';
export enum UserRole {
Admin = 'admin',
Attendee = 'attendee'
}
export type AuthResponse = ApiResponse<{user: User, access_token: string, refresh_token: string}>;
export type RegisterResponse = ApiResponse<{id: string}>;
export type User = {
id: number
email: string
role: UserRole
createdAt: string
updatedAt: string
}
這邊為了開發方便,所以先定義的 url 為本機的 url,正式環境則會另外更改。可以發現這邊會根據不同的手機作不同的設定。而透過 axios 的 interceptor ,可以設定一些預設的基礎邏輯,比如讀取的 data 回應的取法。
import AsyncStorage from '@react-native-async-storage/async-storage';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { Platform } from 'react-native';
const url = Platform.OS === 'android' ? 'http://10.0.2.2:3000':'http://127.0.0.1:3000';
const Api: AxiosInstance = axios.create({baseURL: url });
Api.interceptors.request.use(async config =>{
const token = await AsyncStorage.getItem('access_token');
if (token) config.headers.set('Authorization', token);
return config;
});
Api.interceptors.response.use(
async (res: AxiosResponse) => res.data,
async (err: AxiosError) => Promise.reject(err)
);
export {Api};
這邊透過 Api 來實做, register 與 login 兩種邏輯。
import { AuthResponse, RegisterResponse } from '@/types/user';
import { Api } from './api';
type Credentials = {
email: string
password: string
}
async function login(credentials: Credentials): Promise<AuthResponse> {
return Api.post('/auth/login', credentials);
}
async function register(credentials: Credentials): Promise<RegisterResponse> {
return Api.post('/auth/register', credentials)
}
const userService = {
login,
register
}
export { userService };
import { userService } from '@/services/user';
import { AuthResponse, RegisterResponse, User } from '@/types/user';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { router } from 'expo-router';
import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react';
interface AuthContextProps {
isLoggedIn: boolean;
isLoadingAuth: boolean;
authenticate: (authMode: 'login'|'register', email: string, password: string) => Promise<void>;
logout: VoidFunction;
user: User | null;
}
const AuthContext = createContext({} as AuthContextProps);
// Create custom useAuth
export function useAuth() {
return useContext(AuthContext);
}
export function AuthenticationProvider({ children }: PropsWithChildren) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [isLoadingAuth, setIsLoadingAuth] = useState(false);
const [user, setUser] = useState<User|null>(null);
useEffect(()=> {
async function checkIfLoggedIn() {
const [token, user] = await Promise.all([
AsyncStorage.getItem('access_token'),
AsyncStorage.getItem('user')]);
if (token && user) {
setIsLoggedIn(true);
setUser(JSON.parse(user));
router.replace("(authed)")
} else {
setIsLoggedIn(false);
}
}
checkIfLoggedIn()
}, []);
async function authenticate(authMode: 'login'|'register', email: string, password: string):Promise<void> {
try {
setIsLoadingAuth(true);
const response = await userService[authMode]({email, password});
if (response) {
if (authMode == 'login') {
const authData: AuthResponse = response as unknown as AuthResponse;
const {user, access_token, refresh_token} = authData.data;
await Promise.all([
AsyncStorage.setItem('access_token', access_token),
AsyncStorage.setItem('refresh_token', refresh_token),
AsyncStorage.setItem('user', JSON.stringify(user))
]);
setUser(user);
router.replace("(authed)")
setIsLoggedIn(true);
} else {
const registerResp = response as RegisterResponse;
const { message } = registerResp;
const { id } = registerResp.data;
console.log({ message, id });
}
}
} catch (error) {
setIsLoggedIn(false);
} finally {
setIsLoadingAuth(false);
}
}
async function logout() {
setIsLoggedIn(false);
await Promise.all([
AsyncStorage.removeItem('access_token'),
AsyncStorage.removeItem('refresh_token'),
AsyncStorage.removeItem('user')
]);
setUser(null);
}
return (
<AuthContext.Provider
value={{
isLoggedIn,
isLoadingAuth,
authenticate,
user,
logout,
}}
>
{children}
</AuthContext.Provider>
)
}
這邊先定義了一個 AuthContext Provider,並且設定了裡面的參數。透過了這個 Provider 就能把這段邏輯,讓其內部的 Component 來使用。
Provider 利用剛剛設定好的 userService 的功能來對 api 發網路請求,並且把處理邏輯寫在對應的 function 。透過 AsyncStorage 來作資料狀態的儲存。
import { AuthenticationProvider } from '@/context/AuthContext';
import { Slot } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
export default function Root() {
return (
<>
<StatusBar style='dark' />
<AuthenticationProvider>
<Slot />
</AuthenticationProvider>
</>
);
}
透過 AuthenticationProvider,可以把 Authentication 邏輯套用在內部的元件上。
import { Button } from '@/components/Button';
import { Divider } from '@/components/Divider';
import { HStack } from '@/components/HStack';
import { Input } from '@/components/Input';
import { TabBarIcon } from '@/components/navigation/TabBarIcon';
import { Text } from '@/components/Text';
import { VStack } from '@/components/VStack';
import { useAuth } from '@/context/AuthContext';
import { useState } from 'react';
import { KeyboardAvoidingView, ScrollView } from 'react-native';
export default function Login() {
const {authenticate, isLoadingAuth } = useAuth();
const [authMode, setAuthMode] = useState<'login'|'register'>('login')
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
async function onAuthenticate() {
await authenticate(authMode, email, password)
}
function onToggleAuthMode() {
setAuthMode(authMode === 'login'? 'register':'login');
}
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{flex: 1}}>
<VStack flex={1} justifyContent='center' alignItems='center' p={40} gap={40}>
<HStack gap={10}>
<Text fontSize={30} bold mb={20}>Ticket Booking</Text>
<TabBarIcon name='ticket' size={50}/>
</HStack>
<VStack w={'100%'} gap={30}>
<VStack gap={5}>
<Text ml={10} fontSize={14} color='gray'>Email</Text>
<Input
value={email}
onChangeText={setEmail}
placeholder='Email'
placeholderTextColor='darkgray'
autoCapitalize='none'
autoCorrect={false}
h={48}
p={14}
/>
</VStack>
<VStack gap={5}>
<Text ml={10} fontSize={14} color='gray'>Password</Text>
<Input
secureTextEntry
value={password}
onChangeText={setPassword}
placeholder='Password'
placeholderTextColor='darkgray'
autoCapitalize='none'
autoCorrect={false}
h={48}
p={14}
/>
</VStack>
</VStack>
<Button
w={'100%'}
isLoading={isLoadingAuth}
onPress={onAuthenticate}
>
{authMode === 'login'? 'Login': 'Register'}
</Button>
<Divider w={'90%'}/>
<Text onPress={onToggleAuthMode} fontSize={16} underline>
{authMode === 'login'? 'Register new account': 'Login to account'}
</Text>
</VStack>
</ScrollView>
</KeyboardAvoidingView>
);
}
透過 inspector 看到註冊成功的訊息
使用剛才的資訊作登入
登入成功的結果
透過 inspector 讀取到的 log
實做 Mobile app 的最複雜之處在於狀態的處理,特別是牽涉到元件生命週期的部份。到這裡剩下了最後關鍵處理 Ticket 的部份了。這部份會在下一篇作詳細處理。